﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Globalization;
using System.Net;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls.WebParts;
using System.Xml;
using Microsoft.SharePoint;
using Microsoft.SharePoint.WebControls;
using Microsoft.SharePoint.Utilities;
using Microsoft.SharePoint.WebPartPages;
using WebPart = System.Web.UI.WebControls.WebParts.WebPart;

namespace Mazin.SPGoogleMapPart
{

    [ToolboxItem(false)]
    public class VisualWebPart : WebPart, IWebPartPageComponentProvider
    {
        private const string HttpUrl = "http://maps.googleapis.com";
        private const string HttpsUrl = "https://maps-api-ssl.google.com";

        private readonly List<Marker> _markers = new List<Marker>();
        private string _addressColumnName;

        // map settings
        private double _centreLatitude;
        private double _centreLongitude;
        private string _descrColumnName;
        private int _height = 300;
        private bool _https;

        private bool _clusterer;
        private string _latitudeColumnName;
        private string _listItemIdParamName;
        private string _listQuery;
        private string _listUrl;
        private string _longitudeColumnName;
        private string _mapType = "HYBRID";
        private string _iconUrl;

        // provider that provides data while connected to other webpart
        private IWebPartTable _provider;
        private string _titleColumnName;
        private string _webUrl;
        private int _width = 400;
        private int _zoom = 1;

        private bool _isSandbox;
        private bool _panControl;
        private bool _zoomControl;
        private bool _maptypeControl;
        private bool _streetviewControl;
        private bool _displayRibbon;

        private string _proxyUserName;
        private string _proxyPassword;
        private string _proxyUrl;

        private string _apiKey;

        private static WebClient addressResolveClient;

        [Personalizable(PersonalizationScope.Shared),WebBrowsable(false)]
        public bool Https
        {
            get { return _https; }
            set { _https = value; }
        }

        [Personalizable(PersonalizationScope.Shared), WebBrowsable(false)]
        public bool UseClusterer
        {
            get { return _clusterer; }
            set { _clusterer = value; }
        }

        [Personalizable(PersonalizationScope.Shared),WebBrowsable(false)]
        public bool PanControl
        {
            get { return _panControl; }
            set { _panControl = value; }
        }

        [Personalizable(PersonalizationScope.Shared), WebBrowsable(false)]
        public bool DisplayRibbon
        {
            get { return _displayRibbon; }
            set { _displayRibbon = value; }
        }

        [Personalizable(PersonalizationScope.Shared),WebBrowsable(false)]
        public bool ZoomControl
        {
            get { return _zoomControl; }
            set { _zoomControl = value; }
        }

        [Personalizable(PersonalizationScope.Shared), WebBrowsable(false)]
        public string ProxyUrl
        {
            get { return _proxyUrl; }
            set { _proxyUrl = value; }
        }

        [Personalizable(PersonalizationScope.Shared), WebBrowsable(false)]
        public string ProxyUserName
        {
            get { return _proxyUserName; }
            set { _proxyUserName = value; }
        }

        [Personalizable(PersonalizationScope.Shared), WebBrowsable(false)]
        public string ProxyPassword
        {
            get { return _proxyPassword; }
            set { _proxyPassword = value; }
        }
        [Personalizable(PersonalizationScope.Shared),WebBrowsable(false)]
        public bool MapTypeControl
        {
            get { return _maptypeControl; }
            set { _maptypeControl = value; }
        }

        [Personalizable(PersonalizationScope.Shared), WebBrowsable(false)]
        public bool StreetViewControl
        {
            get { return _streetviewControl; }
            set { _streetviewControl = value; }
        }

        [WebBrowsable(false)]
        private string ApiUrl
        {
            get { return _https ? HttpsUrl : HttpUrl; }
        }

        [WebBrowsable(false)]
        public string ApiKey
        {
            get
            {
                return _apiKey;
            }
            set
            {
                _apiKey = value;
            }
        }

        [Category("Map"), Personalizable(PersonalizationScope.Shared),
         WebDescription("Latitude coordinate of the map centre"), WebBrowsable(false),
         WebDisplayName("Map Centre Latitude")]
        public double CentreLatitude
        {
            get { return _centreLatitude; }
            set { _centreLatitude = value; }
        }

        [WebDisplayName("Map Centre Longitude"), Personalizable(PersonalizationScope.Shared), WebBrowsable(false),
         WebDescription("Longitude coordinate of the map centre"), Category("Map")]
        public double CentreLongitude
        {
            get { return _centreLongitude; }
            set { _centreLongitude = value; }
        }

        [WebDescription("Map Height"), Personalizable(PersonalizationScope.Shared), Category("Map"), WebBrowsable(false)
        , WebDisplayName("Map Height")]
        public int MapHeight
        {
            get { return _height; }
            set { _height = value; }
        }

        [WebDescription("Map Type"), Personalizable(PersonalizationScope.Shared), WebBrowsable(false),
         WebDisplayName("Map type"), Category("Map")]
        public string MapType
        {
            get { return _mapType; }
            set { _mapType = value; }
        }

        [WebBrowsable(false), WebDisplayName("Map width"), Category("Map"), WebDescription("Map Width"),
         Personalizable(PersonalizationScope.Shared)]
        public int MapWidth
        {
            get { return _width; }
            set { _width = value; }
        }

        [WebDescription("Zoom Level"), Personalizable(PersonalizationScope.Shared), WebBrowsable(false),
         WebDisplayName("Zoom level"), Category("Map")]
        public int ZoomLevel
        {
            get { return _zoom; }
            set { _zoom = value; }
        }

        [WebDescription("Icon URL"), Personalizable(PersonalizationScope.Shared), WebBrowsable(false),
         WebDisplayName("Icon URL"), Category("Map")]
        public string IconUrl
        {
            get { return _iconUrl; }
            set { _iconUrl = value; }
        }

        [WebDescription("Name of address column"), Personalizable(PersonalizationScope.Shared), WebBrowsable(false),
         WebDisplayName("Address column"), Category("Connection")]
        public string AddressColumn
        {
            get { return _addressColumnName; }
            set { _addressColumnName = value; }
        }

        [WebDescription("Name of longitude column"), Personalizable(PersonalizationScope.Shared), WebBrowsable(false),
         WebDisplayName("Longitude column"), Category("Connection")]
        public string LongitudeColumn
        {
            get { return _longitudeColumnName; }
            set { _longitudeColumnName = value; }
        }

        [WebDescription("Name of latitude column"), Personalizable(PersonalizationScope.Shared), WebBrowsable(false),
         WebDisplayName("Latitude column"), Category("Connection")]
        public string LatitudeColumn
        {
            get { return _latitudeColumnName; }
            set { _latitudeColumnName = value; }
        }

        [WebDescription("Name of description column"), Personalizable(PersonalizationScope.Shared), WebBrowsable(false),
         WebDisplayName("Description column"), Category("Connection")]
        public string DescriptionColumn
        {
            get { return _descrColumnName; }
            set { _descrColumnName = value; }
        }

        [WebDescription("Name of title column"), Personalizable(PersonalizationScope.Shared), WebBrowsable(false),
         WebDisplayName("Title column"), Category("Connection")]
        public string TitleColumn
        {
            get { return _titleColumnName; }
            set { _titleColumnName = value; }
        }

        [WebDescription("Url of Source list"), Personalizable(PersonalizationScope.Shared), WebBrowsable(false),
         WebDisplayName("List URL"), Category("Connection")]
        public string ListUrl
        {
            get { return _listUrl; }
            set { _listUrl = value; }
        }

        [WebDescription("Url of Source list"), Personalizable(PersonalizationScope.Shared), WebBrowsable(false),
         WebDisplayName("Web URL"), Category("Connection")]
        public string WebUrl
        {
            get { return _webUrl; }
            set { _webUrl = value; }
        }

        [WebDescription("CAML query applied on the source list"), Personalizable(PersonalizationScope.Shared),
         WebBrowsable(false), WebDisplayName("List Query"), Category("Connection")]
        public string ListQuery
        {
            get { return _listQuery; }
            set { _listQuery = value; }
        }

        [WebDescription("Name of URL Id parameter for item to be displayed"),
         Personalizable(PersonalizationScope.Shared), WebBrowsable(false), WebDisplayName("List item Id parameter name")
        , Category("Connection")]
        public string ListItemIdParamName
        {
            get { return _listItemIdParamName; }
            set { _listItemIdParamName = value; }
        }

        [WebDescription("Mode to get data"), Personalizable(PersonalizationScope.Shared), WebBrowsable(false),
         WebDisplayName("Data mode"), Category("Connection")]
        public int DataMode { get; set; }

        private readonly string contextualTab = @"
<ContextualGroup Color=""Blue""
     Command=""SPGoogleMapPart.EnableContextualGroup""
     Id=""Ribbon.ConnectedWebPartTabGroup""
     Title=""{0}""
     Sequence=""502""
     ContextualGroupId=""ConnectedWebPartTabGroup"">
          <Tab
              Id=""Ribbon.ConnectedWebPartTab""
              Title=""{1}""
              Command=""SPGoogleMapPart.EnableCustomTab""
              Sequence=""501"">
            <Scaling
              Id=""Ribbon.SPGoogleMapPart.Scaling"">
              <MaxSize
                Id=""Ribbon.CustomTabExample.MaxSize""
                GroupId=""Ribbon.SPGoogleMapPart.ZoomGroup""
                Size=""LargeMedium""/>
              <Scale
                Id=""Ribbon.CustomTabExample.Scaling.CustomTabScaling""
                GroupId=""Ribbon.SPGoogleMapPart.ZoomGroup""
                Size=""LargeMedium"" />
              <MaxSize
                Id=""Ribbon.CustomTabExample.MapTypeGroupMaxSize""
                GroupId=""Ribbon.SPGoogleMapPart.MapTypeGroup""
                Size=""LargeMedium""/>
              <Scale
                Id=""Ribbon.CustomTabExample.Scaling.MapTypeGroupScaling""
                GroupId=""Ribbon.SPGoogleMapPart.MapTypeGroup""
                Size=""LargeMedium"" />
              <MaxSize
                Id=""Ribbon.CustomTabExample.PanGroupMaxSize""
                GroupId=""Ribbon.SPGoogleMapPart.PanGroup""
                Size=""LargeMedium""/>
              <Scale
                Id=""Ribbon.CustomTabExample.Scaling.PanGroupScaling""
                GroupId=""Ribbon.SPGoogleMapPart.PanGroup""
                Size=""LargeMedium"" />
            </Scaling>
            <Groups Id=""Ribbon.SPGoogleMapPart.Groups"">
              <Group
                Id=""Ribbon.SPGoogleMapPart.ZoomGroup""
                Title=""{2}""
                Command=""SPGoogleMapPart.EnableZoomGroup""
                Sequence=""51""
                Template=""Ribbon.Templates.Flexible2"">
                <Controls Id=""Ribbon.SPGoogleMapPart.ZoomGroup.Controls"">
                  <Button
                    Id=""Ribbon.SPGoogleMapPart.ZoomGroup.ZoomIn""
                    Sequence=""1""
                    Command=""ZoomInCommand""
                    Image32by32=""/_layouts/images/SPGoogleMapPart/zoomin.png""
                    LabelText=""{3}""
                    TemplateAlias=""o1""
                  />
                  <Button
                    Id=""Ribbon.SPGoogleMapPart.ZoomGroup.ZoomOut""
                    Sequence=""2""
                    Command=""ZoomOutCommand""
                    Image32by32=""/_layouts/images/SPGoogleMapPart/zoomout.png""
                    LabelText=""{4}""
                    TemplateAlias=""o1""
                  />
            </Controls>
              </Group>
              <Group
                Id=""Ribbon.SPGoogleMapPart.MapTypeGroup""
                Title=""{9}""
                Command=""SPGoogleMapPart.EnableMapTypeGroup""
                Sequence=""52""
                Template=""Ribbon.Templates.Flexible2"">
                <Controls Id=""Ribbon.SPGoogleMapPart.MapTypeGroup.Controls"">
                  <Button
                    Id=""Ribbon.SPGoogleMapPart.MapTypeGroup.Hybrid""
                    Sequence=""1""
                    Command=""HybridCommand""
                    Image32by32=""/_layouts/images/SPGoogleMapPart/hybrid.png""
                    LabelText=""{10}""
                    ToolTipTitle=""{5}""
                    TemplateAlias=""o1""
                  />
                  <Button
                    Id=""Ribbon.SPGoogleMapPart.MapTypeGroup.RoadMap""
                    Sequence=""2""
                    Command=""RoadMapCommand""
                    Image32by32=""/_layouts/images/SPGoogleMapPart/roadmap.png""
                    LabelText=""{11}""
                    ToolTipTitle=""{6}""
                    TemplateAlias=""o1""
                  />
                  <Button
                    Id=""Ribbon.SPGoogleMapPart.MapTypeGroup.Satellite""
                    Sequence=""3""
                    Command=""SatelliteCommand""
                    Image32by32=""/_layouts/images/SPGoogleMapPart/satellite.png""
                    LabelText=""{12}""
                    ToolTipTitle=""{7}""
                    TemplateAlias=""o1""
                  />
                  <Button
                    Id=""Ribbon.SPGoogleMapPart.MapTypeGroup.Terrain""
                    Sequence=""4""
                    Command=""TerrainCommand""
                    Image32by32=""/_layouts/images/SPGoogleMapPart/terrain.png""
                    LabelText=""{13}""
                    ToolTipTitle=""{8}""
                    TemplateAlias=""o1""
                  />
            </Controls>
              </Group>
              <Group
                Id=""Ribbon.SPGoogleMapPart.PanGroup""
                Title=""{14}""
                Command=""SPGoogleMapPart.EnablePanGroup""
                Sequence=""52""
                Template=""Ribbon.Templates.Flexible2"">
                <Controls Id=""Ribbon.SPGoogleMapPart.PanGroup.Controls"">
                  <Button
                    Id=""Ribbon.SPGoogleMapPart.PanGroup.Left""
                    Sequence=""1""
                    Command=""LeftCommand""
                    Image32by32=""/_layouts/images/SPGoogleMapPart/panleft.png""
                    LabelText=""{15}""
                    TemplateAlias=""o1""
                  />
                  <Button
                    Id=""Ribbon.SPGoogleMapPart.PanGroup.Up""
                    Sequence=""2""
                    Command=""UpCommand""
                    Image32by32=""/_layouts/images/SPGoogleMapPart/panup.png""
                    LabelText=""{16}""
                    TemplateAlias=""o1""
                  />
                  <Button
                    Id=""Ribbon.SPGoogleMapPart.PanGroup.Down""
                    Sequence=""3""
                    Command=""DownCommand""
                    Image32by32=""/_layouts/images/SPGoogleMapPart/pandown.png""
                    LabelText=""{17}""
                    TemplateAlias=""o1""
                  />
                  <Button
                    Id=""Ribbon.SPGoogleMapPart.PanGroup.Right""
                    Sequence=""4""
                    Command=""RightCommand""
                    Image32by32=""/_layouts/images/SPGoogleMapPart/panright.png""
                    LabelText=""{18}""
                    TemplateAlias=""o1""
                  />
            </Controls>
              </Group>
            </Groups>
          </Tab>
   </ContextualGroup>";

        private const string contextualTabTemplate = @"
      <GroupTemplate Id=""Ribbon.Templates.Flexible2"">
        <Layout Title=""LargeMedium"">
          <OverflowSection Type=""OneRow"" TemplateAlias=""o1"" DisplayMode=""Large""/>
          <OverflowSection Type=""ThreeRow"" TemplateAlias=""o2"" DisplayMode=""Medium""/>
        </Layout>
        <Layout Title=""Popup"" LayoutTitle=""LargeMedium"" />
      </GroupTemplate>";


        /// <summary>
        ///   IWebPartTable consumer method.
        /// </summary>
        /// <param name = "provider">IWebPartTable provider.</param>
        [ConnectionConsumer("Table")]
        public void GetTableProvider(IWebPartTable provider)
        {
            _provider = provider;
        }

        /// <summary>
        ///   Method uses configured data source to get list of markers to be displayed
        /// </summary>
        private void GetData()
        {
            DataTable table = null;

            using (new SPMonitoredScope("Getting data"))
            {
                if (_provider != null)
                {
                    DataView data = null;

                    DataFormWebPart webpart = (DataFormWebPart)_provider;
                    string command = ((SPDataSource)webpart.DataSource).SelectCommand;
                    SPQuery q = new SPQuery();
                    q.Query = command;
                    SPListItemCollection items = ((SPDataSource)webpart.DataSource).List.GetItems(q);
                    foreach (SPListItem item in items)
                    {
                    }

                    _provider.GetTableData(d =>
                    {
                        data = (DataView)d;
                        table = data.ToTable();
                    });
                    if (data == null) return;

                }
                else
                {
                    if (string.IsNullOrEmpty(_listUrl)) return;
                    SPWeb web = null;
                    try
                    {
                        if (_isSandbox)
                        {
                            web = SPContext.Current.Web;
                        }
                        else
                        {
                            web = string.IsNullOrEmpty(_webUrl) ? SPContext.Current.Web : SPContext.Current.Site.OpenWeb(_webUrl);
                        }
                        SPList list = web.GetList(_listUrl);
                        var query = new SPQuery { Query = _listQuery };
                        if (!string.IsNullOrEmpty(_listItemIdParamName) && Page.Request.Params[_listItemIdParamName] != null)
                        {
                            string itemId = Page.Request.Params[_listItemIdParamName];
                            query.Query = string.Format("<Where><Eq><FieldRef Name=\"ID\" /><Value Type=\"Counter\">{0}</Value></Eq></Where>", itemId);
                        }
                        SPListItemCollection items = list.GetItems(query);
                        table = items.GetDataTable();
                    }
                    finally
                    {
                        if (web != null && web != SPContext.Current.Web) web.Dispose();
                    }
                }
            }

            using (new SPMonitoredScope("Processing columns"))
            {

                if (table != null && (!string.IsNullOrEmpty(_addressColumnName) || (!string.IsNullOrEmpty(_latitudeColumnName) && !string.IsNullOrEmpty(_longitudeColumnName))))
                {
                    bool hasLatitude = !string.IsNullOrEmpty(_latitudeColumnName) && table.Columns.Contains(_latitudeColumnName);
                    bool hasLongitude = !string.IsNullOrEmpty(_longitudeColumnName) && table.Columns.Contains(_longitudeColumnName);

                    bool hasAddress = !string.IsNullOrEmpty(_addressColumnName) && table.Columns.Contains(_addressColumnName);
                    bool hasDescription = !string.IsNullOrEmpty(_descrColumnName) && table.Columns.Contains(_descrColumnName);
                    bool hasTitle = !string.IsNullOrEmpty(_titleColumnName) && table.Columns.Contains(_titleColumnName);

                    var tmps = new List<Marker>();
                    Dictionary<string,Marker> addressCache = new Dictionary<string,Marker>();
                    WebProxy proxy = null;
                    if (!string.IsNullOrEmpty(_proxyUrl))
                    {
                        proxy = new WebProxy(_proxyUrl);
                        if (!string.IsNullOrEmpty(_proxyUserName) && !string.IsNullOrEmpty(_proxyPassword))
                            proxy.Credentials = new NetworkCredential(_proxyUserName, _proxyPassword);
                        else proxy.Credentials = CredentialCache.DefaultNetworkCredentials;
                    }
                    
                    for (int i = 0; i < table.Rows.Count; i++)
                    {
                        DataRow row = table.Rows[i];
                        var tmp = new Marker();

                        if (hasLatitude && hasLongitude) FillCoordinates(row, tmp);

                        if (hasAddress && tmp.Latitude == 0 && tmp.Longitude == 0)
                        {
                            tmp.Address = (string)row[_addressColumnName];
                            if (addressCache.ContainsKey(tmp.Address))
                            {
                                tmp.Latitude = addressCache[tmp.Address].Latitude;
                                tmp.Longitude = addressCache[tmp.Address].Longitude;
                            }
                            else ResolveAddress(_https, tmp, addressCache, proxy, _apiKey);
                        }

                        if (!string.IsNullOrEmpty(_iconUrl))
                        {
                            if (row.Table.Columns.Contains(_iconUrl))
                            {
                                tmp.Icon = row[_iconUrl] as string;
                                if (!string.IsNullOrEmpty(tmp.Icon)) tmp.Icon = string.Format(", icon:\"{0}\"", tmp.Icon.Split(',')[0]);
                            }
                            else
                            {
                                tmp.Icon = string.Format(", icon:\"{0}\"", _iconUrl);
                            }
                        }

                        if (hasTitle) tmp.Title = (string)row[_titleColumnName];
                        if (hasDescription) tmp.Description = row[_descrColumnName].ToString();

                        if (tmp.Latitude != 0.0 && tmp.Longitude != 0.0) tmps.Add(tmp);

                    }
                    if (tmps.Count > 0) _markers.AddRange(tmps);
                }
            }
        }

        /// <summary>
        ///   Fill marker's coordinates from source data row
        /// </summary>
        private void FillCoordinates(DataRow row, Marker marker)
        {
                double dPos;
                bool lngFound = false;
                string strPos = row[_latitudeColumnName].ToString();
                bool parseOk = double.TryParse(strPos, NumberStyles.Number, CultureInfo.CurrentCulture, out dPos);
                if (!parseOk) parseOk = double.TryParse(strPos, NumberStyles.Number, CultureInfo.InvariantCulture, out dPos);
                if (!parseOk)
                {
                    var r =
                        new Regex(
                            @"(?<first>(((?<degrees>\d+)\xb0(?<minutes>\d+)[′'](?<seconds>\d+)[″\x22])(?<quadrant>[NSEW]))|((?<degrees>\d+\x2E\d+)(?<quadrant>[NSEW]))|((?<sign>[-]){0,1}(?<degrees>\d+\x2E\d+))|((?<sign>[-]){0,1}(?<degrees>\d+)\xb0\s(?<minutes>\d+\x2E\d+))|(((?<degrees>\d+)d\s(?<minutes>\d+)[′']\s(?<seconds>\d+)[″\x22]\s)(?<quadrant>[NSEW])))((,{0,1}\s)(?<lng>(((?<degrees>\d+)\xb0(?<minutes>\d+)[′'](?<seconds>\d+)[″\x22])(?<quadrant>[NSEW]))|((?<degrees>\d+\x2E\d+)(?<quadrant>[NSEW]))|((?<sign>[-]){0,1}(?<degrees>\d+\x2E\d+))|((?<sign>[-]){0,1}(?<degrees>\d+)\xb0\s(?<minutes>\d+\x2E\d+))|(((?<degrees>\d+)d\s(?<minutes>\d+)[′']\s(?<seconds>\d+)[″\x22]\s)(?<quadrant>[NSEW]))){0,1}){0,1}",
                            RegexOptions.Singleline | RegexOptions.ExplicitCapture);
                    Match match = r.Match(strPos);
                    if (match.Success)
                    {
                        string sign = match.Groups["sign"].Success ? match.Groups["sign"].Captures[0].Value : string.Empty;
                        string quadrant = match.Groups["quadrant"].Success
                                              ? match.Groups["quadrant"].Captures[0].Value
                                              : string.Empty;
                        string degrees = match.Groups["degrees"].Captures[0].Value;
                        string minutes = match.Groups["minutes"].Success
                                             ? match.Groups["minutes"].Captures[0].Value
                                             : "0";
                        string seconds = match.Groups["seconds"].Success
                                             ? match.Groups["seconds"].Captures[0].Value
                                             : "0";
                        marker.Latitude = ConstructCoordinate(sign, quadrant, degrees, minutes, seconds);
                        if (match.Groups["lng"].Success)
                        {
                            lngFound = true;
                            sign = match.Groups["sign"].Success ? match.Groups["sign"].Captures[1].Value : string.Empty;
                            quadrant = match.Groups["quadrant"].Success ? match.Groups["quadrant"].Captures[1].Value : string.Empty;
                            degrees = match.Groups["degrees"].Captures[1].Value;
                            minutes = match.Groups["minutes"].Success ? match.Groups["minutes"].Captures[1].Value : "0";
                            seconds = match.Groups["seconds"].Success ? match.Groups["seconds"].Captures[1].Value : "0";
                            marker.Longitude = ConstructCoordinate(sign, quadrant, degrees, minutes, seconds);
                        }
                    }
                }
                else marker.Latitude = dPos;
                if (!lngFound)
                {
                    strPos = row[_longitudeColumnName].ToString();
                    parseOk = double.TryParse(strPos, NumberStyles.Number, CultureInfo.CurrentCulture, out dPos);
                    if (!parseOk) parseOk = double.TryParse(strPos, NumberStyles.Number, CultureInfo.InvariantCulture, out dPos);
                    if (!parseOk)
                    {
                        var r =
                            new Regex(
                                @"(?<first>(((?<degrees>\d+)\xb0(?<minutes>\d+)[′'](?<seconds>\d+)[″\x22])(?<quadrant>[NSEW]))|((?<degrees>\d+\x2E\d+)(?<quadrant>[NSEW]))|((?<sign>[-]){0,1}(?<degrees>\d+\x2E\d+))|((?<sign>[-]){0,1}(?<degrees>\d+)\xb0\s(?<minutes>\d+\x2E\d+))|(((?<degrees>\d+)d\s(?<minutes>\d+)[′']\s(?<seconds>\d+)[″\x22]\s)(?<quadrant>[NSEW])))((,{0,1}\s)(?<lng>(((?<degrees>\d+)\xb0(?<minutes>\d+)[′'](?<seconds>\d+)[″\x22])(?<quadrant>[NSEW]))|((?<degrees>\d+\x2E\d+)(?<quadrant>[NSEW]))|((?<sign>[-]){0,1}(?<degrees>\d+\x2E\d+))|((?<sign>[-]){0,1}(?<degrees>\d+)\xb0\s(?<minutes>\d+\x2E\d+))|(((?<degrees>\d+)d\s(?<minutes>\d+)[′']\s(?<seconds>\d+)[″\x22]\s)(?<quadrant>[NSEW]))){0,1}){0,1}",
                                RegexOptions.Singleline | RegexOptions.ExplicitCapture);
                        Match match = r.Match(strPos);
                        if (match.Success)
                        {
                            string sign = match.Groups["sign"].Success ? match.Groups["sign"].Captures[0].Value : string.Empty;
                            string quadrant = match.Groups["quadrant"].Success
                                                  ? match.Groups["quadrant"].Captures[0].Value
                                                  : string.Empty;
                            string degrees = match.Groups["degrees"].Captures[0].Value;
                            string minutes = match.Groups["minutes"].Success
                                                 ? match.Groups["minutes"].Captures[0].Value
                                                 : "0";
                            string seconds = match.Groups["seconds"].Success
                                                 ? match.Groups["seconds"].Captures[0].Value
                                                 : "0";
                            marker.Longitude = ConstructCoordinate(sign, quadrant, degrees, minutes, seconds);
                        }
                    }
                    else marker.Longitude = dPos;
                }
        }

        /// <summary>
        ///   Construct decimal representation used by Google from structured form of coordinate
        /// </summary>
        private static double ConstructCoordinate(string sign, string quadrant, string degrees, string minutes, string seconds)
        {
            double d = double.Parse(degrees);
            double m = double.Parse(minutes);
            double s = double.Parse(seconds);
            double retValue = d + m / 60 + s / 3600;
            if (sign == "-" || quadrant == "S" || quadrant == "W") retValue *= -1;
            return retValue;
        }

        /// <summary>
        ///   This method resolve markers address to coordinates using Google Geocode API.
        /// </summary>
        private static void ResolveAddress(bool https, Marker marker, Dictionary<string,Marker> cache, IWebProxy proxy, string apiKey)
        {
            using (new SPMonitoredScope("Address resolving"))
            {
                if (addressResolveClient == null)
                {
                    addressResolveClient = new WebClient();
                    if (https) ServicePointManager.ServerCertificateValidationCallback += BypassAllCertificateStuff;
                }

                if (proxy != null)
                {
                    addressResolveClient.Proxy = proxy;
                }

                string result = addressResolveClient.DownloadString(string.Format("{0}/maps/api/geocode/xml?address={1}&sensor=false", https ? HttpsUrl : HttpUrl, HttpUtility.UrlEncode(marker.Address)));
                var xmlDocument = new XmlDocument();
                xmlDocument.LoadXml(result);
                XmlNode status = xmlDocument.SelectSingleNode("//GeocodeResponse/status");
                if (status != null && status.InnerText == "OK")
                {
                    XmlNode latNode = xmlDocument.SelectSingleNode("/GeocodeResponse/result/geometry/location/lat");
                    XmlNode lngNode = xmlDocument.SelectSingleNode("/GeocodeResponse/result/geometry/location/lng");
                    marker.Latitude =
                        double.Parse(
                        latNode != null ? latNode.InnerText : "0.0",
                            CultureInfo.InvariantCulture);
                    marker.Longitude =
                        double.Parse(
                        lngNode != null ? lngNode.InnerText : "0.0",
                            CultureInfo.InvariantCulture);
                    cache.Add(marker.Address, marker);
                }
            }
        }

        private static bool BypassAllCertificateStuff(object sender, X509Certificate cert, X509Chain chain, SslPolicyErrors error)
        {
            return true;
        }

        public override EditorPartCollection CreateEditorParts()
        {
            var editorParts = new ArrayList();
            var editor = new MapEditorPart { ID = ID + "_editorPart" };
            editorParts.Add(editor);
            return new EditorPartCollection(editorParts);
        }

        protected override void CreateChildControls()
        {
            _isSandbox = AppDomain.CurrentDomain.FriendlyName.Contains("Sandbox");
            try
            {
                if (_clusterer)
                {
                    Controls.Add(new LiteralControl(
                        string.Format(CultureInfo.InvariantCulture, "<script type=\"text/javascript\" src=\"_layouts/spgooglemappart/markerclusterer{0}_compiled.js\"></script>", Https ? "https" : string.Empty)));
                }

                GetData();

                if (_centreLongitude == 0 && _centreLatitude == 0) CalculateCentre(out _centreLatitude, out _centreLongitude);

                var canvas =
                    new LiteralControl(
                        string.Format("<meta name=\"viewport\" content=\"initial-scale=1.0, user-scalable=no\" /><script type=\"text/javascript\" src=\"{2}/maps/api/js?sensor=false{3}\"></script><div id=\"map_canvas\" style=\"width:{0}px;height:{1}px;\"></div>",
                            _width, _height, ApiUrl, string.IsNullOrEmpty(_apiKey) ? string.Empty : "&key=" + _apiKey));
                var scripts =
                    new LiteralControl(
                        string.Format(CultureInfo.InvariantCulture, "<script type=\"text/javascript\">var map;\nfunction ginitialize() {{ var myLatlng = new google.maps.LatLng({0}, {1});var myOptions = {{mapTypeControlOptions: {{style: google.maps.MapTypeControlStyle.DROPDOWN_MENU}},zoom: {2}, center: myLatlng,mapTypeId: google.maps.MapTypeId.{3}, panControl: {4}, zoomControl: {5}, mapTypeControl: {6}, streetViewControl :{7} }};map = new google.maps.Map(document.getElementById(\"map_canvas\"), myOptions);}}\n ginitialize();</script>",
                            _centreLatitude, _centreLongitude, _zoom, _mapType, _panControl ? "true" : "false", _zoomControl ? "true" : "false", _maptypeControl ? "true" : "false", _streetviewControl ? "true" : "false"));

                Controls.Add(canvas);
                Controls.Add(scripts);

                using (new SPMonitoredScope("Generate Markers script"))
                {
                    GenerateMarkersScript(_markers.ToArray());
                }
            }
            catch (Exception ex)
            {
                if (!DesignMode) this.Page.ClientScript.RegisterStartupScript(this.GetType(), "error", string.Format("<script>window.alert(\"{0}\")</script>", ex.Message.Replace("\"", "\\\"")));
            }
        }

        protected override void OnPreRender(EventArgs e)
        {
            base.OnPreRender(e);
            if (_displayRibbon)
            {
                this.AddContextualTab();
                ClientScriptManager clientScript = this.Page.ClientScript;
                clientScript.RegisterClientScriptBlock(this.GetType(), "SPGoogleMapPart", this.DelayScript);
            }
        }

        public string DelayScript
        {

            get
            {
                string webPartPageComponentId = SPRibbon.GetWebPartPageComponentId(this);

                return @"
<script type=""text/javascript"">
//<![CDATA[
            function _addCustomPageComponent()
            {
                var _customPageComponent = new SPGoogleMapPart.CustomPageComponent('" + webPartPageComponentId + @"');
                SP.Ribbon.PageManager.get_instance().addPageComponent(_customPageComponent);
            }

            function _registerCustomPageComponent()
            {
                SP.SOD.registerSod(""SPGoogleMapPartPageComponent.js"", ""/_layouts/SPGoogleMapPart/SPGoogleMapPartPageComponent.js"");
                SP.SOD.executeFunc(""SPGoogleMapPartPageComponent.js"", ""SPGoogleMapPart.CustomPageComponent"", _addCustomPageComponent);
            }

            SP.SOD.executeOrDelayUntilScriptLoaded(_registerCustomPageComponent, ""sp.ribbon.js"");
//]]>
</script>";
            }
        }


        private void GenerateMarkersScript(Marker[] markers)
        {
            if (markers == null || markers.Length == 0) return;

            var itemsSetupScript = new StringBuilder("<script type=\"text/javascript\">var infoW = null;var markers = [];");

            for (int i = 0; i < markers.Length; i++)
            {
                Marker marker = markers[i];
                itemsSetupScript.AppendFormat(CultureInfo.InvariantCulture,
                    "var mr{3} = new google.maps.Marker({{position: new google.maps.LatLng({1}, {2}),{5} title: \"{0}\" {4}}});",
                    string.IsNullOrEmpty(marker.Title) ? string.Empty : marker.Title.Replace('\n', ' ').Replace("\"", "\\\""),
                    marker.Latitude,
                    marker.Longitude,
                    i,
                    string.IsNullOrEmpty(marker.Icon) ? string.Empty : marker.Icon,
                    _clusterer ? string.Empty : "map : map,");

                if (!string.IsNullOrEmpty(marker.Description)) itemsSetupScript.AppendFormat(
                    CultureInfo.InvariantCulture,
                    "google.maps.event.addListener(mr{1}, 'click', function () {{if (infoW!=null) infoW.close(); infoW = new google.maps.InfoWindow({{content: \"{0}\"}});infoW.open(map, mr{1});}});",
                    marker.Description.Replace('\n', ' ').Replace('\r', ' ').Replace("\"", "\\\""),
                    i);
                if (_clusterer)
                {
                    itemsSetupScript.AppendFormat("markers.push(mr{0});",i);
                }

            }
            if (_clusterer)
            {
                itemsSetupScript.Append("var markerCluster = new MarkerClusterer(map, markers);");
            }
            itemsSetupScript.Append("</script>");
            
            Controls.Add(new LiteralControl(itemsSetupScript.ToString()));
        }

        /// <summary>
        ///   When center point is not configured, this method is used to calculate it using found markers
        /// </summary>
        private void CalculateCentre(out double cLat, out double cLng)
        {
            double maxLat = -200, minLat = 200;
            double maxLng = -200, minLng = 200;

            foreach (Marker marker in _markers)
            {
                if (maxLat < marker.Latitude) maxLat = marker.Latitude;
                if (minLat > marker.Latitude) minLat = marker.Latitude;
                if (maxLng < marker.Longitude) maxLng = marker.Longitude;
                if (minLng > marker.Longitude) minLng = marker.Longitude;
            }

            cLat = (maxLat + minLat) / 2;
            cLng = (maxLng + minLng) / 2;
        }

        private void AddContextualTab()
        {
            uint lang = (uint)System.Threading.Thread.CurrentThread.CurrentUICulture.LCID;
            string conttab = String.Format(contextualTab,
                SPUtility.GetLocalizedString("$Resources:SPGoogleMapPart,ContextualGroupTitle",null,lang),
                SPUtility.GetLocalizedString("$Resources:SPGoogleMapPart,TabTitle",null,lang),
                SPUtility.GetLocalizedString("$Resources:SPGoogleMapPart,ZoomGroupTitle",null,lang),
                SPUtility.GetLocalizedString("$Resources:SPGoogleMapPart,ZoomInCommandLabel",null,lang),
                SPUtility.GetLocalizedString("$Resources:SPGoogleMapPart,ZoomOutCommandLabel", null, lang),
                SPUtility.GetLocalizedString("$Resources:SPGoogleMapPart,EditorMapTypeHybrid", null, lang),
                SPUtility.GetLocalizedString("$Resources:SPGoogleMapPart,EditorMapTypeRoadMap", null, lang),
                SPUtility.GetLocalizedString("$Resources:SPGoogleMapPart,EditorMapTypeSatellite", null, lang),
                SPUtility.GetLocalizedString("$Resources:SPGoogleMapPart,EditorMapTypeTerrain", null, lang),
                SPUtility.GetLocalizedString("$Resources:SPGoogleMapPart,EditorMapTypeLabel", null, lang),
                SPUtility.GetLocalizedString("$Resources:SPGoogleMapPart,MapTypeHybrid", null, lang),
                SPUtility.GetLocalizedString("$Resources:SPGoogleMapPart,MapTypeRoadMap", null, lang),
                SPUtility.GetLocalizedString("$Resources:SPGoogleMapPart,MapTypeSatellite", null, lang),
                SPUtility.GetLocalizedString("$Resources:SPGoogleMapPart,MapTypeTerrain", null, lang),
                SPUtility.GetLocalizedString("$Resources:SPGoogleMapPart,PanGroupTitle", null, lang),
                SPUtility.GetLocalizedString("$Resources:SPGoogleMapPart,PanLeftCommanLabel", null, lang),
                SPUtility.GetLocalizedString("$Resources:SPGoogleMapPart,PanUpCommanLabel", null, lang),
                SPUtility.GetLocalizedString("$Resources:SPGoogleMapPart,PanDownCommanLabel", null, lang),
                SPUtility.GetLocalizedString("$Resources:SPGoogleMapPart,PanRightCommanLabel", null, lang)

            );

            // Gets the current instance of the ribbon on the page.
            Microsoft.Web.CommandUI.Ribbon ribbon = SPRibbon.GetCurrent(this.Page);
            // Prepares an XmlDocument object used to load the ribbon extensions.
            XmlDocument ribbonExtensions = new XmlDocument();
            // Load the contextual tab XML and register the ribbon extension.
            ribbonExtensions.LoadXml(conttab);
            ribbon.RegisterDataExtension(ribbonExtensions.FirstChild, "Ribbon.ContextualTabs._children");

            // Load the custom templates and register the ribbon extension.
            ribbonExtensions.LoadXml(contextualTabTemplate);
            ribbon.RegisterDataExtension(ribbonExtensions.FirstChild, "Ribbon.Templates._children");
        }

        public WebPartContextualInfo WebPartContextualInfo
        {
            get
            {
                WebPartContextualInfo info = new WebPartContextualInfo();
                WebPartRibbonContextualGroup contextualGroup = new WebPartRibbonContextualGroup();
                WebPartRibbonTab ribbonTab = new WebPartRibbonTab();

                // Create the contextual group object and initialize its values.
                contextualGroup.Id = "Ribbon.ConnectedWebPartTabGroup";
                contextualGroup.Command = "SPGoogleMapPart.EnableContextualGroup";
                contextualGroup.VisibilityContext = "SPGoogleMapPart.CustomVisibilityContext";

                // Create the tab object and initialize its values.
                ribbonTab.Id = "Ribbon.ConnectedWebPartTab";
                ribbonTab.VisibilityContext = "SPGoogleMapPart.CustomVisibilityContext";

                // Add the contextual group and tab to the WebPartContextualInfo.
                info.ContextualGroups.Add(contextualGroup);
                info.Tabs.Add(ribbonTab);
                info.PageComponentId = SPRibbon.GetWebPartPageComponentId(this);

                return info;
            }
        }
    }
}